一、前言
1. 什么是 AOP ?
AOP 是 Aspect-oriented programming 的缩写,即面向切面编程,是一种编程范式。通过它可以在不修改原有代码的情况下向现有代码中添加某些行为。例如:当函数名称以 “set” 开头时,记录所有函数的调用。这允许将非业务逻辑的核心行为添加到程序中,而不会使代码与原有业务逻辑耦合在一起。
2. Android 项目中如何实现 AOP ?
Android 官方使用 Gradle 作为应用的构建工具,与其强大的可扩展性密不可分。Android 官方依赖 Gradle 提供了一个 com.android.tools.build:gradle:$version
plugin,这个 Plugin 想必大家都不陌生,它就是 AGP 。AGP 在 1.5.0-beta1 的时候添加了 Transform API ,支持第三方 plugins 在将编译好的 class 文件转换为 dex 文件之前对其进行操作。
熟悉 Android 打包流程的应该都看过下面这张图:
Transform API 作用时机就是在 Compilers -> DEX File(s) 这个过程中。
Transform API 工作流程如下:
即上一个 Transform 的输出作为下一个 Transform 的输入。我们可以通过自定义 Plugin 注册 Transform API 来获取上一个 Transform 的输入,然后按照自己的需求,对相应的 class 文件做修改,然后交给下一个 Transform 作为输入继续后面的流程。
而修改 class 文件需要借助字节码操作框架来实现,目前主流的字节码操作框架有如下几种:ASM,Javassist,AspectJ,ByteBuddy,Lancet 等。ByteBuddy 目前并未使用过,暂不做评价。Lancet 是 eleme 开源的一款轻量级的 Android 平台的字节码操作框架。原理也是基于 AGP Transform API 扫描 class 文件,底层使用 ASM 实现插桩,使用方式上类似 AspectJ,不过 Lancet 目前处于无人维护的状态,所使用 AGP 版本比较旧(AGP 3.3.2),如果想使用,需要自己 fork 升级 AGP 版本并解决升级过程的错误,其实可以考虑暂不做考虑。另外值得一提的是,ReDex 是 Fackbook 开源的一个针对 Android Dex 文件优化的一个库,使用 C++ 实现,它提供了一系列指令生成 API 和 Opcode 插入 API,我们可以借助它实现自己的字节码注入工具,但是其上手难度很高,暂不考虑。
二、ASM, Javassist, AspectJ
1. ASM
ASM是一个通用的 Java 字节码操作和分析框架。它可以用来修改现有的类或直接以二进制形式动态生成类。ASM 提供了一些常见的字节码转换和分析算法,从中可以构建定制的复杂转换和代码分析工具。ASM 提供了与其他 Java 字节码框架相似的功能,但重点是性能。由于它被设计和实现得尽可能小、尽可能快,因此非常适合在动态系统中使用(当然也可以以静态方式使用,例如在编译器中)。
ASM用于许多项目,包括:
- OpenJDK,以生成 lambda 调用位置,也可以在 Nashorn 编译器中生成;
- Groovy 编译器和 Kotlin 编译器;
- Cobertura 和 Jacoco,为了测量代码覆盖率;
- Byte Buddy,用于动态生成类,它本身用于其他项目,如 Mockito(用于生成模拟类);
- Gradle,在运行时生成一些类。
优缺点:
- 操作灵活。支持任意字节码的操作。支持 visitor api 和 tree api 两种方式;
- 高性能;
- 学习曲线陡峭。ASM 是非常底层的面向字节码编程的 AOP 框架。需要掌握 Java 字节码的相关知识才能使用。
2. Javassist
Javassist(Java Programming Assistant)使 Java 字节码操作变得简单。它是一个用 Java 编辑字节码的类库;它使 Java 程序能够在运行时定义新类,并在 JVM 加载时修改类文件。与其他类似的字节码编辑器不同,Javassist 提供了两级 API:源代码级和字节码级。如果用户使用源代码级 API,他们可以在不了解 Java 字节码规范的情况下编辑类文件。整个 API 仅使用 Java 语言的词汇表进行设计。您甚至可以以源文本的形式指定插入的字节码;Javassist 实时编译它。另一方面,字节码级 API 允许用户像其他编辑器一样直接编辑类文件。
优缺点:
- 易上手。提供了高封装度的 api,只需要了解很少的字节码知识就可以使用;
- 性能低。使用到反射机制,性能比较低。
3. AspectJ
- Java 编程语言的无缝面向切面的扩展;
- 兼容 Java 平台;
- 易于学习和使用;
- 横切关注点的干净模块化,例如错误检查和处理、同步、上下文相关行为、性能优化、监视和日志记录、调试支持以及多对象协议。
优缺点:
- 成熟稳定。一般不用考虑插入的字节码正确性的问题;
- 易上手。使用者完全不需要理解任何 Java 字节码相关的知识,就可以使用自如。它可以在方法(含构造方法)被调用位置、方法体(含构造方法)内部、读写变量位置、静态代码块内部、异常处理位置等前后,插入自定义代码,或者直接将原位置的代码替换为自己的代码;
- 切入点固定。只能在一些固定的切入点进行操作,如果想要更加细致的操作,则无法完成。不能针对一些特定规则的字节码序列做操作;
- 正则表达式。AspectJ 使用正则表达式匹配规则,比如匹配 Activity 生命周期的 onXXX 方法,如果有自定义的其他的以 on 开头的方法也会被匹配到;
- 性能较低。AspectJ 插入的代码会包装自己的一些类,逻辑比较复杂,不仅生成的字节码比较大,对原函数的性能也会造成一定的影响。
4. 对比
ASM | Javassist | AspectJ | |
---|---|---|---|
易用性 | 难 | 易 | 易 |
性能 | 高 | 低 | 低 |
自由度 | 高 | 低 | 低 |
三、自定义 Gradle Plugin
介绍完 Transform API 的机制和各种 AOP 框架的优劣后,我们就可以选择适合我们的框架,编写插桩代码,然后将其集成到 Android 项目的构建流程中,实现构建时自动化插桩。
那么如何集成到 Gradle 的构建流程中呢?答案就是自定义 Gradle Plugin。
创建 Gradle Plugin 有以下三种方式:
- 在 gradle script 中创建。gradle script 中一般用来配置一些静态配置,不建议使用这种方式创建 plugin;
- 在
buildSrc
模块中创建。一般用来创建项目内的 plugin,不用发布,可以在 script 中直接引用; - 在独立的模块中创建。一般用来创建可发布到公有仓库,供其他项目使用的 plugin。
本文示例使用第二种方式创建:
在项目根目录下创建一个名为
buildSrc
的目录,注意,1. 是创建目录,不是创建 module,2. buildSrc 大小写要一致;在目录中创建 build.gradle.kts 脚本文件,如果是使用 java 编写 plugin,就引入
java-library
插件,如果是用 groovy 编写,就引入groovy
插件,如果是用 kotlin 编写,就引入kotlin("jvm")
插件,我这里使用 kotlin 来编写插件;然后在dependencies
block 中添加gradleApi()
。如果只是创建一个 Gradle Plugin,到这里 build.gradle.kts 其实就已经配置好了;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15plugins {
// `java-library`
// groovy
kotlin("jvm") version "1.7.10"
}
repositories {
google()
mavenCentral()
}
dependencies {
implementation(kotlin("stdlib"))
gradleApi()
}创建 src/main/kotlin 目录,再创建自定义的 Plugin 类,实现自
org.gradle.api.Plugin
接口:1
2
3
4
5
6
7
8import org.gradle.api.Plugin
import org.gradle.api.Project
class SamplePlugin : Plugin<Project> {
override fun apply(target: Project) {
println("SamplePlugin>>apply")
}
}在 application 或 library module 中 apply plugin:
1
2// app module's build.gradle.kts
apply<SamplePlugin>()sync project:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28> Task :buildSrc:compileKotlin UP-TO-DATE
> Task :buildSrc:compileJava NO-SOURCE
> Task :buildSrc:compileGroovy NO-SOURCE
> Task :buildSrc:processResources NO-SOURCE
> Task :buildSrc:classes UP-TO-DATE
> Task :buildSrc:inspectClassesForKotlinIC UP-TO-DATE
> Task :buildSrc:jar UP-TO-DATE
> Task :buildSrc:assemble UP-TO-DATE
> Task :buildSrc:compileTestKotlin NO-SOURCE
> Task :buildSrc:compileTestJava NO-SOURCE
> Task :buildSrc:compileTestGroovy NO-SOURCE
> Task :buildSrc:processTestResources NO-SOURCE
> Task :buildSrc:testClasses UP-TO-DATE
> Task :buildSrc:test NO-SOURCE
> Task :buildSrc:check UP-TO-DATE
> Task :buildSrc:build UP-TO-DATE
> Configure project :app
SamplePlugin>>apply
> Task :prepareKotlinBuildScriptModel UP-TO-DATE
BUILD SUCCESSFUL in 1s
3 actionable tasks: 3 up-to-date
> Task :prepareKotlinBuildScriptModel UP-TO-DATE
BUILD SUCCESSFUL in 105ms
顺便说一下这里的
UP-TO-DATE
:Gradle 之所以能够在 Project 上工作,都是由一个个的 Task 所支持的。Task 表示构建执行的一些基本操作。例如创建 Jar,生成 Javadoc 或者发布一些 Archives 到仓库。而大多数 Task 都声明了输入和输出,Gradle 根据检查 Task 的输入和输出来确定该 Task 是否是UP-TO-DATE
的。例如:
compile
task 的输入是 source code,如果 source code 自从上次 compile 以来没有任何变化,然后会检查输出,确保编译器生成的 class 文件没有被破坏,如果输入和输出都没有任何变化,Gradle 认为这个 Task 是UP-TO-DATE
的,这使得在构建项目时可以节省大量的时间。
我们可以看到 build 控制台中已经输出了我们在 Plugin 中打印的内容了。到这里,一个完整的 Plugin 创建步骤就完成了,这是一个只依赖于 Gradle Api 的 Plugin,我们可以在里面创建 Task 等。但是,因为我们要借助 AGP 来开发我们的 AOP Plugin,所以,我们还需要在 buildSrc
build.gradle.kts 中添加 AGP 和相关 AOP 框架的依赖:
1 | dependencies { |
四、AGP Transform API
AGP Transform API 在 AGP 7.0 的时候被标记为废弃,预计在 AGP 8.0 中会被删除。取而代之的是 Gradle 提供的
TransformAction
以及 AGP 在新版本中提供的相关 API。那我们为什么还要学它呢?
- 社区沉淀:AGP Transform API 已经发展多年,社区中已经沉淀了大量优秀的开源项目以及博客文章;
- 思维方式:虽然 API 已过时,但学习其中的思路,对理解 Gradle TransformAction 有一定的帮助。
优秀的涉及到 AGP Transform API 相关的开源项目:Lancet、StringFog、一些插件化/组件化/路由框架
优秀的涉及到 Gradle TransformAction 相关的开源项目:BRouter
- 获取
BaseExtension
1 | // def android = project.android |
registerTransform
1 | android.registerTransform(XXXTransform()) |
- Transform
Transform 是用来处理构建过程中的中间产物。每添加一个 Transform,都会对应创建一个 Task,该 Task 的命名规则为 transform${inputTypes}With${name}For${variant}
。
String getName()
:指定 Transform 的名称
Set<ContentType> getInputTypes()
:返回当前 Transform 处理的数据的类型
Set<? super Scope> getScopes()
:返回当前 Transform 处理的数据的范围
boolean isIncremental()
:指定当前 Transform 是否开启增量编译,如果开启的话,需要自己在 transform 时处理好增量的逻辑
void transform(@NonNull TransformInvocation transformInvocation)
:在该方法中实现字节码插桩操作
TransformInvocation
中包含了所有的输入和输出信息,Transform 的输入是一个 TransformInput
的集合,TransformInput
中又包含了 JarInput
的集合和 DirectoryInput
的集合,这两者都提供了具体的输入内容,以及与其关联的 ContentType
和 Scope
信息。
Transform 的输出通过 TransformOutputProvider 获取。
Transform 的输入和输出均由 AGP 内部处理,并且它们的位置不可配置。
⚠️ 重要提示:即使我们不想 transform 任何东西,我们也需要将所有的输入 copy 到输出,否则最终生成的 APK 中不会包含这些文件。
直接看代码吧:https://github.com/porum/AgpBytecodeManipulationDemo
五、Variant/Artifact API
六、总结
AOP 可以降低项目的耦合度,对项目源代码无入侵,提高开发效率,但是需要掌握一定的字节码相关的知识,以及字节码插桩框架的使用,一定程度上限制了人们对其了解的冲动。还是希望感兴趣的可以行动起来,迈开第一步,就会打开新世界的大门。
七、参考
- https://asm.ow2.io/
- http://www.javassist.org/
- https://www.eclipse.org/aspectj/doc/released/progguide/index.html
- https://developer.android.google.cn/studio/releases/gradle-plugin-api-updates#transform-removed
- http://tools.android.com/tech-docs/new-build-system/transform-api
- https://developer.android.google.cn/studio/build#build-process
- https://source.android.google.cn/docs/core/runtime/dalvik-bytecode
- https://medium.com/grandcentrix/transform-api-a-real-world-example-cfd49990d3e1
- https://rebooters.github.io/2020/01/04/Gradle-Transform-ASM-%E6%8E%A2%E7%B4%A2/
- https://segmentfault.com/a/1190000041861621